#!/usr/bin/env python
#
# mp3tom4b.py - convert directory of .mp3 to .m4b - iPod audiobook format
#
# Copyright (C) 2008 Alexander 'zowers' Petrov (zowers@zowers.net)
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# usage: python mp3tom4b.py [-i indir] [-o outdir]
#
# requires:
# * python          http://python.org/
# * kaa.metadata    http://sourceforge.net/projects/freevo/ (debian package python-kaa-metadata)
# * tagpy           http://http://mathema.tician.de/software/tagpy (debian package python-tagpy)
# * madplay         http://sourceforge.net/projects/mad/
# * faac            http://sourceforge.net/projects/faac/

import logging
import os
import sys
import subprocess
import optparse
import re
import kaa.metadata
import tagpy
import pprint

MADPLAY = "madplay"
FAAC = "faac"

def main( argv ):
    parser = optparse.OptionParser()
    parser.set_defaults( indir = os.getcwd() )
    parser.set_defaults( outdir = None )
    parser.set_defaults( overwrite = False )
    parser.set_defaults( verbose = True )
    parser.add_option( "-i", "--indir", dest="indir" )
    parser.add_option( "-o", "--outdir", dest="outdir" )
    parser.add_option( "-w", "--overwrite", dest="overwrite", action="store_true" )
    parser.add_option( "-q", "--quiet", dest="verbose", action="store_false" )
    ( options, args ) = parser.parse_args()
    typemap = { "mp3": "m4b" }
    options.outdir = options.outdir or options.indir
    convert_dir_with_subdirs( options.indir, options.outdir, options.overwrite, options.verbose, typemap )

def convert_dir_with_subdirs( indir, outdir, overwrite, verbose, typemap ):
    if not os.path.exists( indir ):
        raise ValueError( "Can't find input directory '%s'" % indir )
    check_madplay_exists()
    check_faac_exists()
    if verbose:
        print "Converting directory '%s'" % indir
    for root, dirs, files in os.walk( indir ):
        dirs.sort()
        subdir = root[ len( os.path.commonprefix( [ indir, root ] ) ) + 1: ]
        convert_dir_files( os.path.join( indir, subdir ), os.path.join( outdir, subdir ), overwrite, verbose, typemap )

def pad_track_number( value ):
    if value is None:
        return value
    try:
        return '%03i' % int( value )
    except ValueError:
        return '%03s' % value

def convert_dir_files( indir, outdir, overwrite, verbose, typemap ):
    if verbose:
        print "Listing '%s'" % indir
    files = os.listdir( indir )
    files.sort()
    for f in files:
        if not re.match( r".*\.%s" % "|".join( typemap.keys() ), f ):
            continue
        src_name, src_ext = os.path.splitext( os.path.basename( f ) )
        if src_ext.startswith( "." ):
            src_ext = src_ext[ 1: ]
        if src_ext not in typemap:
            continue
        dst_name = src_name + "." + typemap[ src_ext ]
        dst_path = os.path.join( outdir, dst_name )
        src_path = os.path.join( indir, f )
        if os.path.exists( dst_path ):
            if not overwrite:
                if verbose:
                    print "Skipping existing '%s'" % dst_path
                continue
        if verbose:
            print "Converting '%s' -> '%s'" % ( src_path, dst_path )
        try:
            read_params = dict()
            read_proc = make_read_process( src_path, MADPLAY, read_params )
            src_info = kaa.metadata.parse( src_path )
            src_info_tagpy_f = tagpy.FileRef( src_path )
            src_info_tagpy = src_info_tagpy_f.tag()
            write_params = dict(
                  genre = src_info.genre
                , artist = src_info.artist
                , album = src_info.album
                , track = ( '%s' % src_info.trackno ) if src_info.trackno is not None else None
                , title = src_info.title
                , comment = src_info.comment or ""
                )
            if not write_params[ "genre" ] and src_info_tagpy.genre:
                write_params[ "genre" ] = src_info_tagpy.genre
            if not write_params[ "artist" ] and src_info_tagpy.artist or write_params[ "artist" ] != src_info_tagpy.artist:
                write_params[ "artist" ] = src_info_tagpy.artist
            if not write_params[ "album" ] and src_info_tagpy.album:
                write_params[ "album" ] = src_info_tagpy.album
            if not write_params[ "track" ] and src_info_tagpy.track:
                write_params[ "track" ] = src_info_tagpy.track
            if not write_params[ "title" ] and src_info_tagpy.title:
                write_params[ "title" ] = src_info_tagpy.title
            if not write_params[ "title" ]:
                write_params[ "title" ] = os.path.splitext( os.path.basename( src_path ) )[ 0 ]
            write_params[ "title" ] = "%s: %s" % ( write_params[ "album" ]
                                                   , ' '.join( filter( None, [ pad_track_number( write_params[ "track" ] ), write_params[ "title" ] ] ) )
                                                   )
            if verbose:
                print "Write params: ",
                pprint.pprint( write_params )
            try:
                os.makedirs( os.path.dirname( dst_path ) )
            except:
                pass
            write_proc = make_write_process( read_proc, dst_path, FAAC, write_params )
            out,err = write_proc.communicate()
            if out:
                print out
            if err:
                print err
        except KeyboardInterrupt:
            sys.exit( "Interrupted" )
        except Exception, e:
            logging.exception( "Error converting '%s': %s" % ( src_path, e ) )

def check_madplay_exists():
    subprocess.check_call( [ MADPLAY, "--version" ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT )

def check_faac_exists():
    try:
        subprocess.check_call( [ FAAC, "--help" ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT )
    except subprocess.CalledProcessError:
        pass

def make_read_process( f, reader, params ):
    if reader == MADPLAY:
        return make_madplay_read_process( f, params )
    else:
        raise ValueError( "Unsupported reader requested: '%s'" % reader )

def make_madplay_read_process( f, params ):
    proc = subprocess.Popen( [ "madplay", "-o", "wave:-", f ], stdout=subprocess.PIPE )
    return proc

def make_write_process( reader, f, writer, params ):
    if writer == FAAC:
        return make_faac_write_process( reader, f, params )
    else:
        raise ValueError( "Unsupported reader requested: '%s'" % reader )

def make_faac_write_process( reader, f, params ):
    proc = subprocess.Popen( [ "faac", "-w"
            , "--genre", "%s" % params[ "genre" ]
            , "--artist", "%s" % params[ "artist" ]
            , "--album", "%s" % params[ "album" ]
            , "--title", "%s" % params[ "title" ]
            , "--comment", "%s" % params[ "comment" ]
            , "-o", f, "-" ]
        , stdin=reader.stdout
        )
    return proc

if __name__ == "__main__":
    sys.exit( main( sys.argv ) )
